package com.thinkbiganalytics.auth.rest; /*- * #%L * REST API Authentication * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.thinkbiganalytics.auth.jaas.AbstractLoginModule; import com.thinkbiganalytics.rest.JerseyRestClient; import com.thinkbiganalytics.security.rest.model.UserPrincipal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.util.Map; import javax.annotation.Nonnull; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.AccountLockedException; import javax.security.auth.login.AccountNotFoundException; import javax.security.auth.login.CredentialException; import javax.security.auth.login.FailedLoginException; import javax.security.auth.spi.LoginModule; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.ProcessingException; import javax.ws.rs.WebApplicationException; /** * Authenticates users by querying an external REST API. This allows the UI module to get a user's groups from the Services module. */ public class KyloRestLoginModule extends AbstractLoginModule implements LoginModule { private static final Logger log = LoggerFactory.getLogger(KyloRestLoginModule.class); /** * Option for the URL of the REST API endpoint */ static final String LOGIN_USER = "loginUser"; /** * Option for the URL of the REST API endpoint */ static final String LOGIN_PASSWORD = "loginPassword"; /** * Option for REST client configuration */ static final String REST_CLIENT_CONFIG = "restClientConfig"; /** * REST API client configuration */ private LoginJerseyClientConfig config; /** * Alternate username (such as service account) to use when making the REST call */ private String loginUser = null; /** * The password to use when a loginUser property is set */ private String loginPassword = null; @Override public void initialize(@Nonnull final Subject subject, @Nonnull final CallbackHandler callbackHandler, @Nonnull final Map<String, ?> sharedState, @Nonnull final Map<String, ?> options) { super.initialize(subject, callbackHandler, sharedState, options); try { config = (LoginJerseyClientConfig) options.get(REST_CLIENT_CONFIG); loginUser = (String) getOption(LOGIN_USER).orElse(null); loginPassword = loginUser == null ? null : (String) getOption(LOGIN_PASSWORD) .orElseThrow(() -> new IllegalArgumentException("A REST login password is required if a login username was provided")); } catch (RuntimeException e) { log.error("Unhandled exception during initialization", e); throw e; } } @Override protected boolean doLogin() throws Exception { // Get username and password final NameCallback nameCallback = new NameCallback("Username: "); final PasswordCallback passwordCallback = new PasswordCallback("Password: ", false); final String username; final String password; if (loginUser == null) { // Use user's own username and password to access the REST API if a loginUser was not provided. handle(nameCallback, passwordCallback); username = nameCallback.getName(); password = new String(passwordCallback.getPassword()); } else { // Using the loginUser to access API so only need the authenticating user's name. handle(nameCallback); username = loginUser; password = loginPassword; } final LoginJerseyClientConfig userConfig = new LoginJerseyClientConfig(config); userConfig.setUsername(username); userConfig.setPassword(password); final UserPrincipal user; try { user = retrieveUser(nameCallback.getName(), userConfig); } catch (final NotAuthorizedException e) { log.debug("Received unauthorized response from Login API for user: {}", username); throw new CredentialException("The username and password combination do not match."); } catch (final ProcessingException e) { log.error("Failed to process response from Login API for user: {}", username, e); throw new FailedLoginException("The login service is unavailable."); } catch (final WebApplicationException e) { log.error("Received unexpected response from Login API for user: {}", username, e); throw new FailedLoginException("The login service is unavailable."); } // Parse response if (user == null) { log.debug("Received null response from Login API for user: {}", username); throw new AccountNotFoundException("No account exists with the name: " + username); } else if (!user.isEnabled()) { log.debug("User from Login API is disabled: {}", username); throw new AccountLockedException("The account \"" + username + "\" is currently disabled"); } addNewUserPrincipal(user.getSystemName()); user.getGroups().forEach(this::addNewGroupPrincipal); return true; } private UserPrincipal retrieveUser(String user, final LoginJerseyClientConfig userConfig) { String endpoint = loginUser == null ? "/v1/about/me" : "/v1/security/users/" + user; return getClient(userConfig).get(endpoint, null, UserPrincipal.class); } @Override protected boolean doCommit() throws Exception { getSubject().getPrincipals().addAll(getAllPrincipals()); return true; } @Override protected boolean doAbort() throws Exception { return doLogout(); } @Override protected boolean doLogout() throws Exception { getSubject().getPrincipals().removeAll(getAllPrincipals()); return true; } /** * Gets a Jersey client using the specified configuration. * * @param config the login configuration * @return the Jersey client */ @Nonnull JerseyRestClient getClient(@Nonnull final LoginJerseyClientConfig config) { return new JerseyRestClient(config); } }